﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using FastMember;
using Platform.Infrastructure;
using Platform.Linq;
using Platform.ObjectModel;
using Platform.ObjectModel.Design;
using Platform.Resources;

namespace Platform.Storage.Entity
{

	/// <summary>
	/// Realiza a materialização dos documentos
	/// </summary>
	public sealed class EntityDataObjectMaterializer //: DataObjectMaterializer
	{

		/// <summary>
		/// 
		/// </summary>
		/// <param name="library"></param>
		public EntityDataObjectMaterializer( Library library )
		{
			Arguments.IsNotNull( "library", library );
			this.Library = library;
		}

		/// <summary>
		/// Obtém ou define a biblioteca de documentos
		/// </summary>
		public Library Library
		{
			get;
			private set;
		}

		/// <summary>
		/// Obtém o tipo .Net do documento que é identificado por <paramref name="schemaName"/>.
		/// </summary>
		/// <param name="schemaName"></param>
		/// <returns></returns>
		public Type GetDocumentType( DataObjectName schemaName )
		{
			return this.Library.Workspace.GetDocType( schemaName );
		}

		/// <summary>
		/// 
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <returns></returns>
		public IDataObjectBuilder CreateBuilder<T>()
		{
			return this.CreateBuilder( typeof( T ) );
		}

		//public IDataObjectBuilder CreateBuilder( Type docType )
		//{
		//	return this.CreateBuilder( null, docType );
		//}

		/// <summary>
		/// Realiza a criação do tipo .Net
		/// </summary>
		/// <param name="docType"></param>
		/// <returns></returns>
		//internal IDataObjectBuilder CreateBuilder( FieldValue firstValue, Type docType)
		public IDataObjectBuilder CreateBuilder( Type docType )
		{
			Arguments.IsNotNull( "docType", docType );
			Schema schema = this.Library.Workspace.GetSchema( docType );

			// Se não o schema pelo Type, tenta
			// carregar pelo nome do schema
			if ( schema == null )
			{
				throw new NotSupportedException();

				// TODO : Realiza implementação para carregar o schema pelo fullname do schema
				// Exemplo: this.Library.Workspace.Get( new DataObjectName( "Product", "Catalog" ) );
			}

			// Se mesmo não encontrando pelo nome do schema,
			// então em uma próxima versão, irá carregar materializar
			// em um objeto genérico
			if ( schema == null )
			{
				throw new NotSupportedException();
			}

			

			// Se está carregando um schema, inicializa de forma diferente
			if ( docType.Equals( typeof( Schema ) ) )
			{
				return new DocumentBuilder( schema, () => new Schema( new DataObjectName( "Unknow" ) ) );
			}
			//else if ( docType.Equals( typeof( FieldDescriptor ) ) )
			//{
			//	Debug.Assert( firstValue != null );

			//	string fieldName = firstValue.FieldName;
			//	return new DocumentBuilder( schema, () => new FieldDescriptor( DataObjectName.Parse( fieldName ) ) );
			//}
			else
			{
				// TODO: Alterar para permitir escolher o melhor construtor para ser utilizado durante a inicialização

				if ( !docType.InheritedFrom( typeof( DocumentFlake ) ) )
					throw Exceptions.TypeIsNotDocument( docType );

				// Encontra o melhor conjunto de parâmetros que possa ser utilizado
				// com os dados atuais
				var constructorParams = this.FindBestConstructorArgs( schema, docType ).ToArray();
				return new DocumentBuilder( schema, () => Activator.CreateInstance( docType, constructorParams ) as DocumentFlake );

				//if ( !docType.InheritedFrom( typeof( DocumentFlake ) ) )
				//	throw Exceptions.TypeIsNotDocument( docType );
				//return new DocumentBuilder( schema, () => Activator.CreateInstance( docType ) as DocumentFlake );
			}

			throw new NotImplementedException();
		}

		private IEnumerable<object> FindBestConstructorArgs( Schema schema, Type forType )
		{
			bool hasEmptyConstructor = false;

			// Se possui um construtor que recebe o nome do qualifica do objeto
			foreach ( var cstr in forType.GetConstructors() )
			{
				if ( cstr.IsPublic && !cstr.IsStatic )
				{
					var parameters = cstr.GetParameters();

					if ( parameters.IsEmpty() )
					{
						hasEmptyConstructor = true;
					}
					else if ( parameters.HasOnlyOne( p => p.ParameterType.Equals( typeof( DataObjectName ) ) ) )
					{
						return new object[] { schema.QualifiedName };
					}
				}
			}

			if ( hasEmptyConstructor )
			{
				return Enumerable.Empty<object>();
			}
			else
			{
				throw Exceptions.DocumentTypeConstructorIsInvalid( forType );
			}
		}

		#region [ class DocumentBuilder ]

		/// <summary>
		/// 
		/// </summary>
		internal sealed class DocumentBuilder : IDataObjectBuilder
		{
			/// <summary>
			/// Obtém a lista de campos do documento
			/// </summary>
			//private List<FieldValue> fields = new List<FieldValue>();

			private DocumentFlake document = null;

			internal DocumentBuilder( Schema schema, Func<DocumentFlake> factory )
			{
				Debug.Assert( schema != null, "O schema não foi encontrado." );
				Debug.Assert( factory != null, "A factory para criação do documento não foi definido." );

				this.Schema = schema;
				this.Factory = factory;
			}

			public Schema Schema
			{ get; private set; }

			public DocumentFlake Document
			{
				get
				{
					if ( this.document == null )
					{
						this.document = this.Factory();
						this.document.SchemaName = this.Schema.QualifiedName;
					}
					return this.document;
				}
			}

			/// <summary>
			/// Obtém a função que realiza a criação da instância do documento
			/// </summary>
			public Func<DocumentFlake> Factory
			{ get; private set; }


			#region IDataObjectBuilder Members

			public void SetField( FieldValue field )
			{
				if ( field.BinValue != null && field.BinValue.Length > 0 )
				{
					// Procura pelo campo no schema
					FieldDescriptor fieldDesc = this.Schema.Fields[ field.FieldName ];

					// Se não encontrou o schema, devo tentar carregar o valor mesmo assim,
					// verificando se existe uma propriedade com esse nome no objeto (estático ou dinâmico) 
					// ou simplesmente ignoro
					//if ( fieldDesc == null )
					//	throw Exceptions.FieldNotFoundInSchema( field.FieldName );
					if ( fieldDesc != null )
					{
						DocumentFlake doc = this.Document;
						Type fieldType = LibraryUtility.GetSystemFieldType( fieldDesc, doc );
						doc[ fieldDesc.QualifiedName ] = fieldDesc.GetSerializer().FromStream( fieldType, field.BinValue );
					}
				}
			}

			public void SetRelation( DataObjectRelationship relationship, DocumentFlake childDocument )
			{
				//this.Schema.Relations[0]
				FieldDescriptor fieldDesc = this.Schema.Relations[ relationship.ParentOriginalFieldName ];
				DataObjectName fieldName = fieldDesc.QualifiedName;

				//this.Schema.Fields[relationship.parent

				Type fieldType = LibraryUtility.GetSystemFieldType( fieldDesc, this.Document );
				
				Type itemType;
				if ( fieldType.IsCollection( out itemType ) )
				{
					// Adicionar na coleção através de reflection
					fieldType.Call( "Add", childDocument ).Of( this.Document[ fieldName ] );
				}
				else
				{
					this.Document[ fieldName ] = childDocument;
				}

			}

			public DataObjectFlake GetDocument()
			{
				return this.Document;

				// Define cada valor do documento
				//foreach ( var value in this.fields )
				//{
				//	//FieldDescriptor field = this.Schema.Fields[ LibraryUtility.ExtractFieldName( value.FieldName ) ];
				//	FieldDescriptor field = this.Schema.Fields[ value.FieldName ];
				//	if ( field == null )
				//		throw Exceptions.FieldNotFoundInSchema( value.FieldName );

				//	Type fieldType = LibraryUtility.GetSystemFieldType( field, document );
				//	document[ field.QualifiedName ] = field.GetSerializer().FromStream( fieldType, value.BinValue );
				//}
				//return document;
			}

			#endregion
		}

		#endregion
	}
}
